﻿
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Xunit;
using Xunit.Extensions;

namespace EmperialApps.WeatherSpark.Data {

    public class TestForecastManager {
#if DEBUG
        private const double ScheduleInterval = 0.01;
#else
        private const double ScheduleInterval = 1.0;
#endif

        [Fact]
        public void ctor_GivenEmptyStore_SchedulesTimer( ) {
            var timer = new MockTimer( );

            var manager = CreateManager( timer: timer );
            var actual = timer.StartInterval;

            Assert.NotEqual( default( TimeSpan ), actual );
        }

        [Fact]
        public void ctor_GivenFilledStoreAndInitializeTrue_AddsLocationsToSchedule( ) {
            var store = new MockStore( 3 );
            var scheduler = new MockScheduler( );
            var expected = store.Forecasts.Keys.ToArray( );

            var manager = CreateManager( store, scheduler, initialize: true );
            var actual = scheduler.Added.OrderBy( c => c );

            Assert.Equal( expected, actual );
        }

        [Fact]
        public void ctor_GivenFilledStoreAndInitializeFalse_DoesNotAddLocationsToSchedule( ) {
            var store = new MockStore( 3 );
            var scheduler = new MockScheduler( );

            var manager = CreateManager( store, scheduler, initialize: false );

            Assert.Empty( scheduler.Added );
        }


        [Fact]
        public void DownloadCompleted_ForNewLocation_SavesDownloadToStore( ) {
            var location = new Coordinate( 1, 0 );
            var expected = new Forecast( location, DefaultDescription, DefaultStart, DefaultData );
            var store = new MockStore( );
            var manager = CreateManager( store );

            store.DownloadCompletedHandlers( store, new ForecastEventArgs( location, expected ) );
            var actual = store.Forecasts[location];

            actual.AssertEqual( expected );
        }

        [Fact]
        public void DownloadCompleted_ForExistingLocation_SavesCombinedForecastToStore( ) {
            var store = new MockStore( 1 );
            var location = store.Forecasts.Keys.Single( );
            var forecast = new Forecast( location, DefaultDescription, DefaultStart.AddHours( 2 ), DefaultData );
            var expected = Forecast.Combine( store.Forecasts[location], forecast );
            var manager = CreateManager( store );

            store.DownloadCompletedHandlers( store, new ForecastEventArgs( location, forecast ) );
            var actual = store.Forecasts[location];

            actual.AssertEqual( expected );
        }

        [Fact]
        public void DownloadCompleted_WhenForecastLocationDiffers_SavesDownloadToStoreAtForecastLocation( ) {
            var specifedLocation = new Coordinate( 1, 0 );
            var forecastLocation = new Coordinate( 1.1, 0 );
            var expected = new Forecast( forecastLocation, DefaultDescription, DefaultStart, DefaultData );
            var store = new MockStore( );
            var manager = CreateManager( store );

            store.DownloadCompletedHandlers( store, new ForecastEventArgs( specifedLocation, expected ) );
            var actual = store.Forecasts[forecastLocation];

            actual.AssertEqual( expected );
        }

        [Theory]
        [InlineData( typeof( FormatException ) )]
        [InlineData( typeof( System.Net.WebException ) )]
        public void DownloadCompleted_WhenDownloadFailsWithException_SavesDownloadToStoreWithoutModfication( Type exceptionType ) {
            var exception = (Exception)Activator.CreateInstance( exceptionType );
            var location = new Coordinate( 1, 0 );
            var store = new MockStore( );
            var manager = CreateManager( store );

            var expected = new ForecastEventArgs( location, null, exception );
            store.DownloadCompletedHandlers( store, expected );
            var actual = store.Saves.Single( );

            Assert.Same( expected, actual );
        }


        [Fact]
        public void Tick_ChecksSchedule( ) {
            var timer = new MockTimer( );
            var store = new MockStore( 3 );
            var scheduler = new MockScheduler { Next = new Coordinate( 1, 0 ) };
            var manager = CreateManager( store, scheduler, timer );

            timer.TickHandlers( timer, EventArgs.Empty );
            Coordinate? actual = scheduler.Next;

            Assert.Equal( default( Coordinate? ), actual );
        }

        [Fact]
        public void Tick_WhenNextItemNotPresent_DelaysTimer( ) {
            var timer = new MockTimer( );
            var manager = CreateManager( timer: timer );
            TimeSpan initial = timer.StartInterval;

            timer.TickHandlers( timer, EventArgs.Empty );
            timer.TickHandlers( timer, EventArgs.Empty );
            timer.TickHandlers( timer, EventArgs.Empty );
            TimeSpan final = timer.StartInterval;
            TimeSpan actual = final - initial;

            Assert.InRange( final - initial, TimeSpan.FromSeconds( 1 ), TimeSpan.FromSeconds( 10 ) );
        }

        [Fact]
        public void Tick_WhenNextItemPresentAfterDelay_RestoresTimer( ) {
            var timer = new MockTimer( );
            var scheduler = new MockScheduler( );
            var manager = CreateManager( null, scheduler, timer );
            TimeSpan expected = timer.StartInterval;

            timer.TickHandlers( timer, EventArgs.Empty );
            timer.TickHandlers( timer, EventArgs.Empty );
            scheduler.Next = new Coordinate( 1, 0 );
            timer.TickHandlers( timer, EventArgs.Empty );
            TimeSpan actual = timer.StartInterval;

            Assert.Equal( expected, actual );
        }


        #region Utility

        private const string DefaultDescription = "Default Description";
        private static readonly DateTimeOffset DefaultStart = new DateTimeOffset( 1111, 11, 11, 11, 0, 0, TimeSpan.FromHours( 11 ) );
        private static readonly ReadOnlyCollection<double> DefaultData = ReadOnly( 1, 2, 3 );

        private static ReadOnlyCollection<double> ReadOnly( params double[] values ) {
            return new ReadOnlyCollection<double>( values );
        }

        private static ForecastManager CreateManager( MockStore store = null, MockScheduler scheduler = null, MockTimer timer = null, bool initialize = true ) {
            var manager = new ForecastManager(
                store ?? new MockStore( ),
                scheduler ?? new MockScheduler( ),
                timer ?? new MockTimer( ),
                initialize );
            return manager;
        }


        private sealed class MockTimer : ITimer {
            public EventHandler TickHandlers;
            public TimeSpan StartInterval;
            public bool WasStopped;

            public event EventHandler Tick {
                add { this.TickHandlers += value; }
                remove { this.TickHandlers -= value; }
            }

            public void Start( TimeSpan interval ) { this.StartInterval = interval; }

            public void Stop( ) { this.WasStopped = true; }
        }

        private sealed class MockScheduler : ILocationScheduler {
            public readonly List<Pair<Coordinate, Coordinate>> Scheduled = new List<Pair<Coordinate, Coordinate>>( );
            public readonly List<Coordinate> Removed = new List<Coordinate>( );
            public readonly List<Coordinate> Added = new List<Coordinate>( );
            public Coordinate? Next;

            public void AddLocation( Coordinate location ) {
                this.Added.Add( location );
            }

            public bool TryGetNext( out Coordinate location ) {
                Coordinate? next = this.Next;
                this.Next = null;

                location = next.GetValueOrDefault( );
                return next.HasValue;
            }

            public void Schedule( Coordinate location, Coordinate newLocation ) {
                this.Scheduled.Add( Pair.Create( location, newLocation ) );
            }

            public void Remove( Coordinate location ) {
                this.Removed.Add( location );
            }
        }

        private sealed class MockStore : IForecastStore {
            public EventHandler<ForecastEventArgs> DownloadCompletedHandlers;
            public readonly Dictionary<Coordinate, Forecast> Forecasts;
            public readonly List<Coordinate> Downloads;
            public readonly List<Coordinate> Loads;
            public readonly List<ForecastEventArgs> Saves;

            public MockStore( int storeCount = 0 ) {
                this.Forecasts = new Dictionary<Coordinate, Forecast>( );
                this.Downloads = new List<Coordinate>( );
                this.Loads = new List<Coordinate>( );
                this.Saves = new List<ForecastEventArgs>( );

                for( int i = 0; i < storeCount; ++i ) {
                    var location = new Coordinate( 0, i );
                    var forecast = new Forecast( location, DefaultDescription, DefaultStart, DefaultData );
                    this.Forecasts.Add( location, forecast );
                }
            }

            public bool Save( ForecastEventArgs e ) {
                this.Saves.Add( e );
                if( e.Error == null ) {
                    Forecast forecast = e.Forecast;
                    this.Forecasts[forecast.Location] = forecast;
                }

                return e.Error == null;
            }

            public Forecast Load( Coordinate location, object context ) {
                Assert.IsType<ForecastEventArgs>( context );
                this.Loads.Add( location );

                Forecast forecast;
                this.Forecasts.TryGetValue( location, out forecast );
                return forecast;
            }

            public IEnumerable<Coordinate> GetStoredLocations( ) {
                return this.Forecasts.Keys.ToArray( );
            }

            public event EventHandler<ForecastEventArgs> DownloadCompleted {
                add { this.DownloadCompletedHandlers += value; }
                remove { this.DownloadCompletedHandlers -= value; }
            }

            public void BeginDownload( Coordinate location ) {
                this.Downloads.Add( location );
            }
        }

        #endregion
    }

}
